﻿/** \file
* \$Rev: 6 $
* 
* \$Date: 2010-06-24 01:39:39 +0000 (Thu, 24 Jun 2010) $
*
* \$URL: http://fofbrainz.googlecode.com/svn/trunk/Program.cs $
*/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using System.Xml.Serialization;
using Microsoft.Win32;

namespace FofBrainz
{
	/// <summary>
	/// The main program class. This does all of the heavy lifting.
	/// </summary>
	static class Program
	{
		/// <summary>The main (and only) application form</summary>
		static MainForm mainform;
		/// <summary>The settings object, loaded from and saved to XML file FofBrainz.exe.config</summary>
		public static readonly Properties.Settings settings = new Properties.Settings();
		/// <summary>The thread that does all of the network operations</summary>
		static Thread workerthread;
		/// <summary>The XML deserializer for MusicBrainz</summary>
		static XmlSerializer xmlser = new XmlSerializer(typeof(MusicBrainz.metadata));

		/// <summary>The regex for song.ini artist parsing</summary>
		static Regex rexartist = new Regex("artist = (.*)\r");
		/// <summary>The regex for song.ini track title parsing</summary>
		static Regex rextrack = new Regex("name = (.*)\r");
		/// <summary>The regex for MusicBrainz image scraping</summary>
		static Regex reximage = new Regex("<td class=\"cover\">.*?<img[^<>]*?src=\"([^\"]*)\"");
		/// <summary>The regex for removing bad file characters</summary>
		static Regex rexbadfilechars = new Regex("[\\<\\>\\:\\|\\?\\*\"]");

		/// <summary>The file stream for classification tests</summary>
		static StreamWriter classtsv;
		/// <summary>True if classification test data generation is enabled</summary>
		public const bool classtest = false;

		/// <summary>The main entry point for the application.</summary>
		[STAThread]
		static void Main()
		{
			if (settings.songdir == null || settings.songdir == "")
				settings.songdir = DefaultSongDir;

			Application.EnableVisualStyles();
			Application.SetCompatibleTextRenderingDefault(false);
			mainform = new MainForm();
			Application.Run(mainform);

			settings.Save();
			if (workerthread != null)
				workerthread.Abort();
		}

		/// <summary>
		/// The default directory that the application should look for FOF songs.
		/// This goes to the registry to look at uninstall information to find
		/// the directory.
		/// </summary>
		public static string DefaultSongDir
		{
			get
			{
				List<string> suffixes = new List<string> { @"Wow6432Node\", "" };
				foreach (string suffix in suffixes)
				{
					RegistryKey instkey = Registry.LocalMachine.OpenSubKey(
						@"SOFTWARE\" + suffix + @"Microsoft\Windows\CurrentVersion\Uninstall\Frets On Fire");
					if (instkey == null) continue;
					string instloc = (string)instkey.GetValue("InstallLocation") + @"\data\songs";
					if (Directory.Exists(instloc))
						return instloc;
				}
				return @"C:\";
			}
		}

		/// <summary>Start the worker thread.</summary>
		public static void StartWorker()
		{
			workerthread = new Thread(Worker);
			workerthread.Start();
		}

		/// <summary>The worker thread function.</summary>
		static void Worker()
		{
			// Search for all song.ini in the song directory tree.
			string[] inifiles = Directory.GetFiles(settings.songdir, "song.ini", SearchOption.AllDirectories);
			mainform.SetProgressMax(inifiles.Length);

			if (classtest)
			{
				if (!Directory.Exists("classtest"))
					Directory.CreateDirectory("classtest");
				classtsv = new StreamWriter(@"classtest\classes.tsv");
			}

			mainform.Log("Found " + inifiles.Length.ToString() + " songs.");
			foreach (string inifile in inifiles)
			{
				string dir = inifile.Substring(0, inifile.LastIndexOf('\\') + 1);
				mainform.Log(dir.Substring(settings.songdir.Length));
				string labelfile = dir + "label.png";
				if (File.Exists(labelfile) && !classtest)
				{
					mainform.Log("\tAlready has a label.");
					continue;
				}

				// Get the current song's artist and track.
				string artistname = "", trackname = "";
				using (FileStream inistream = new FileStream(inifile, FileMode.Open))
				using (StreamReader inireader = new StreamReader(inistream))
				{
					string initext = inireader.ReadToEnd();
					artistname = rexartist.Match(initext).Groups[1].Value;
					trackname = rextrack.Match(initext).Groups[1].Value;
				}

				// Go to the internet to get an image.
				if (ProcessTrack(labelfile, artistname, trackname))
					mainform.Log("\tImage saved!");
				else
					mainform.Log("\tNo good matches.");
				mainform.Tick();
			}
			mainform.Log("Done!");
		}

		/// <summary>Get cover art for a single track.</summary>
		/// <param name="labelfile">The image filename to which the art will be saved.</param>
		/// <param name="artistname">The track's artist name.</param>
		/// <param name="trackname">The track title.</param>
		/// <returns>True on success.</returns>
		static bool ProcessTrack(string labelfile, string artistname, string trackname)
		{
			MusicBrainz.metadata results;

			// Do an XML track search on MusicBrainz.
			string response = PersistentHTTP<string>(
				"http://musicbrainz.org/ws/1/track/?artist=" + Uri.EscapeDataString(artistname) +
				"&track=" + Uri.EscapeDataString(trackname) +
				"&limit=" + settings.maxtries.ToString() + "&type=xml");
			using (StringReader sreader = new StringReader(response))
				results = (MusicBrainz.metadata)xmlser.Deserialize(sreader);

			if (results != null && results.TrackList != null &&
				results.TrackList.track != null)
			{
				if (classtest)
				{
					// Output all results and images to a directory for classification testing
					foreach (MusicBrainz.track track in results.TrackList.track)
					{
						foreach (MusicBrainz.release release in track.ReleaseList.release)
						{
							string imgfile = @"classtest\" + artistname + " - " + trackname +
								" (" + release.id + ").png";
							if (!File.Exists(imgfile))
							{
								if (SaveImage(release, imgfile))
								{
									classtsv.Write(
										artistname + '\t' +
										trackname + '\t' +
										track.artist.name + '\t' +
										track.title + '\t' +
										release.id + '\t' +
										track.id + '\t' +
										release.type + '\t' +
										LevenshteinDistance.Compute(artistname, track.artist.name).ToString() + '\t' +
										LevenshteinDistance.Compute(trackname, track.title).ToString() + '\t' +
										track.score + "\t\n");
								}
							}
						}
					}
					return true;
				}
				// Sort the track results.
				List<MusicBrainz.RankedTrack> ranked = results.GetRankedTracks(artistname, trackname);

				foreach (MusicBrainz.RankedTrack track in ranked)
					if (SaveImage(track.release, labelfile))
						return true;
			}
			return false;
		}

		/// <summary>Download and save an image based on release information</summary>
		/// <param name="release">The MusicBrainz release object associated with the image to download</param>
		/// <param name="labelfile">The image filename to save to</param>
		/// <returns>True if an image was saved</returns>
		static bool SaveImage(MusicBrainz.release release, string labelfile)
		{
			// Scrape the MusicBrainz album page for cover art.
			string releasestr = PersistentHTTP<string>(
				"http://musicbrainz.org/release/" + release.id + ".html");

			GroupCollection groups = reximage.Match(releasestr).Groups;
			if (groups.Count <= 1)
				return false;

			// Download the cover art.
			Bitmap bitmap = PersistentHTTP<Bitmap>(groups[1].Value);
			// Sometimes, MusicBrainz cover art is a phony 1x1 image.
			if (bitmap.Size.Width <= 5 || bitmap.Size.Height <= 5)
				return false;

			// Sanitize filename and save
			string cleanlabelfile = rexbadfilechars.Replace(labelfile, "");
			bitmap.Save(cleanlabelfile, ImageFormat.Png);
			return true;
		}

		/// <summary>Persistently (and synchronously) try an HTTP download.</summary>
		/// <typeparam name="ReturnType">The type of object to fetch, string or Bitmap.</typeparam>
		/// <param name="url">The URL to fetch.</param>
		/// <returns>The downloaded item.</returns>
		static ReturnType PersistentHTTP<ReturnType>(string url)
		{
			int tries = 0;
			while (true)
			{
				try
				{
					// Do the network operation.
					WebRequest request = HttpWebRequest.Create(url);
					request.Timeout = 30000;
					object result = null;
					using (WebResponse response = request.GetResponse())
					using (Stream stream = response.GetResponseStream())
					{
						if (typeof(ReturnType) == typeof(string))
						{
							using (StreamReader reader = new StreamReader(stream))
								result = (object)reader.ReadToEnd();
						}
						else if (typeof(ReturnType) == typeof(Bitmap))
							result = (object)new Bitmap(stream);
					}
					return (ReturnType)result;
				}
				catch (Exception we)
				{
					if (!(we is IOException || we is WebException))
						throw;
					// Catch a failure.
					mainform.Log("\tThe internet failed: " + we.Message);
					if (we.InnerException != null)
						mainform.Log("\t" + we.InnerException.Message);
					if (++tries > 5)
					{
						mainform.Log("\tGiving up.");
						return default(ReturnType);
					}
					if (we is WebException && ((WebException)we).Status != WebExceptionStatus.Timeout)
					{
						mainform.Log("\tWaiting 30s before trying again.");
						Thread.Sleep(30000);
					}
					// If we get here, it means we're looping to try again.
				}
			}
		}
	}
}
